﻿
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using RaytracerLib.Rays;
using TomanuExtensions;
using System;
using YAXLib;
using System.Linq;

namespace RaytracerLib.MathLib
{
    public struct AABB
    {
        public static AABB POINT = new AABB(
            new Vector3(-Constants.MINIMAL_DISTANT, -Constants.MINIMAL_DISTANT, -Constants.MINIMAL_DISTANT), 
            new Vector3(Constants.MINIMAL_DISTANT, Constants.MINIMAL_DISTANT, Constants.MINIMAL_DISTANT));

        public static AABB INFINITY = new AABB(
            new Vector3(Constants.DOUBLE_MIN, Constants.DOUBLE_MIN, Constants.DOUBLE_MIN),
            new Vector3(Constants.DOUBLE_MAX, Constants.DOUBLE_MAX, Constants.DOUBLE_MAX));

        [YAXNode]
        public readonly Vector3 Minimum;

        [YAXNode]
        public readonly Vector3 Maximum;

        public AABB(Vector3 a_minimum, Vector3 a_maximum)
        {
            Minimum = a_minimum;
            Maximum = a_maximum;

            Debug.Assert(Minimum.IsFinite);
            Debug.Assert(Maximum.IsFinite);

            Debug.Assert(Minimum <= Maximum);
        }

        public AABB(params Vector3[] m_vectors)
        {
            Debug.Assert(m_vectors.Length > 2);

            Minimum = Vector3.POSITIVE_INFINITY;
            Maximum = Vector3.NEGATIVE_INFINITY;

            foreach (var v in m_vectors)
            {
                Minimum = Vector3.Minimize(Minimum, v);
                Maximum = Vector3.Maximize(Maximum, v);
            }
        }

        public override string ToString()
        {
            return String.Format("Min: {0}, Max: {1}", Minimum, Maximum);
        }

        public override bool Equals(object a_obj)
        {
            if (a_obj == null)
                return false;

            if (!typeof(AABB).Equals(a_obj.GetType()))
                return Equals((AABB)a_obj);
            else
                return false;
        }

        public bool Equals(AABB a_aabb)
        {
            return (Minimum == a_aabb.Minimum) && (Maximum == a_aabb.Maximum);
        }

        public override int GetHashCode()
        {
            return Minimum.GetHashCode() ^ Maximum.GetHashCode();
        }

        public static bool operator ==(AABB a_left, AABB a_right)
        {
            return a_left.Minimum == a_right.Minimum && a_left.Maximum == a_right.Maximum;
        }

        public static bool operator !=(AABB a_left, AABB a_right)
        {
            return !(a_left == a_right);
        }

        public bool IsIntersecting(Ray a_ray)
        {
            return GetDist(a_ray) != Double.PositiveInfinity;
        }

        public double GetDist(Ray a_ray)
        {
            return GetDist(a_ray.Start, a_ray.Dir);
        }

        public double GetDist(Vector3 a_ray_start, Vector3 a_ray_dir)
        {
            double near, far;
            if (!GetIntersections(a_ray_start, a_ray_dir, out near, out far))
                return Double.PositiveInfinity;
            else
                return near;
        }

        public bool GetIntersections(Ray a_ray, out double a_near, out double a_far)
        {
            return GetIntersections(a_ray.Start, a_ray.Dir, out a_near, out a_far);
        }

        public bool GetIntersections(Vector3 a_ray_start, Vector3 a_ray_dir, out double a_near, out double a_far)
        {
            Debug.Assert(Minimum <= Maximum);

            a_near = Constants.DOUBLE_MIN;
            a_far = Constants.DOUBLE_MAX;

            if (a_ray_dir.X.IsAlmostRelativeEquals(0))
            {
                if ((a_ray_start.X <= Minimum.X) || (a_ray_start.X >= Maximum.X))
                    return false;
            }
            else
            {
                double d1 = (Minimum.X - a_ray_start.X) / a_ray_dir.X;
                double d2 = (Maximum.X - a_ray_start.X) / a_ray_dir.X;

                a_far = Math.Max(d1, d2);

                if (a_far < 0)
                    return false;

                a_near = Math.Min(d1, d2);
            }

            if (a_ray_dir.Y.IsAlmostRelativeEquals(0))
            {
                if ((a_ray_start.Y <= Minimum.Y) || (a_ray_start.Y >= Maximum.Y))
                    return false;
            }
            else
            {
                double d1 = (Minimum.Y - a_ray_start.Y) / a_ray_dir.Y;
                double d2 = (Maximum.Y - a_ray_start.Y) / a_ray_dir.Y;

                a_far = Math.Min(a_far, Math.Max(d1, d2));

                if (a_far < 0)
                    return false;

                a_near = Math.Max(a_near, Math.Min(d1, d2));

                if (a_near > a_far)
                    return false;
            }

            if (a_ray_dir.Z.IsAlmostRelativeEquals(0))
            {
                if ((a_ray_start.Z <= Minimum.Z) || (a_ray_start.Z >= Maximum.Z))
                    return false;
            }
            else
            {
                double d1 = (Minimum.Z - a_ray_start.Z) / a_ray_dir.Z;
                double d2 = (Maximum.Z - a_ray_start.Z) / a_ray_dir.Z;

                a_far = Math.Min(a_far, Math.Max(d1, d2));

                if (a_far < 0)
                    return false;

                a_near = Math.Max(a_near, Math.Min(d1, d2));

                if (a_near > a_far)
                    return false;
            }

            return true;
        }

        public bool Intersect(AABB a_aabb)
        {
            var L = Math.Max(Minimum.X, a_aabb.Minimum.X);
            var R = Math.Min(Maximum.X, a_aabb.Maximum.X);
            var B = Math.Max(Minimum.Y, a_aabb.Minimum.Y);
            var T = Math.Min(Maximum.Y, a_aabb.Maximum.Y);
            var F = Math.Max(Minimum.Z, a_aabb.Minimum.Z);
            var N = Math.Min(Maximum.Z, a_aabb.Maximum.Z);

            return R >= L && T >= B && F >= N;
        }

        public static AABB Merge(AABB a_aabb1, AABB a_aabb2)
        {
            return new AABB(Vector3.Min(a_aabb1.Minimum, a_aabb2.Minimum),
                Vector3.Max(a_aabb1.Maximum, a_aabb2.Maximum));
        }

        public bool IsFinite
        {
            get
            {
                return Minimum.IsFinite && Maximum.IsFinite;
            }
        }

        public Vector3 GetIntersection(Ray a_ray)
        {
            return GetIntersection(a_ray.Start, a_ray.Dir);
        }

        public Vector3 GetIntersection(Vector3 a_ray_start, Vector3 a_ray_dir)
        {
            return a_ray_start + a_ray_dir * GetDist(a_ray_start, a_ray_dir);
        }
    }
}
